#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<assert.h>
#include<errno.h>
#include<string.h>
#include<ctype.h>

#define NUM 1024
#define OPT_NUM 64

#define NONE_REDIR 0
#define INPUT_REDIR 1
#define OUTPUT_REDIR 2
#define APPEND_REDIR 3
//isspace：判断是否是空格,头文件为：#include<ctype.h>
#define  trimSpace(start) do{\
      while(isspace(*start)) start++;\
}while(0) //用do while(0)来包含代码块,这个宏函数就是说*start遇到空格++


char LineCommand[NUM];
char* myargv[OPT_NUM]; //指针数组,用于存放切割后的字符串
int lastSig=0;  //获取最近一次进程的退出信号
int lastCode=0; //获取最近一次进程的退出码

int redirType=NONE_REDIR; //默认情况下，我们的shell是没有重定向的，初始化为0
char* redirFile=NULL; //要指向重定向的文件的起始位置


//扫描命令
//重定向："ls -a -l -i > myfile.txt" -> "ls -a -l -i" "myfile.txt"
void commandCheck(char* commands) //扫描
{
    assert(commands);
    char* start=commands;
    char* end=commands+strlen(commands);

    while(start<end)
    {
        if(*start=='>')
        {
            //重定向："ls -a -l -i > myfile.txt" -> "ls -a -l -i" "myfile.txt"
            *start='\0';
            start++;
            if(*start=='>')
            {
                //到这说明是追加重定向
                //"ls -a >> file.txt"
                redirType=APPEND_REDIR;
                start++;
            }
            else 
            {
                //说明是输出重定向
                //"ls -a > file.txt"
                redirType=OUTPUT_REDIR;
            }
            trimSpace(start);
            redirFile=start;
            break;


        }
        else if(*start=='<')
        {
            //"cat <  file.txt"
            *start='\0';
            start++;
            trimSpace(start);//过滤空格的宏函数
            //填写重定向信息
            redirType=INPUT_REDIR;
            redirFile=start;
            break;
        }
        else 
        {
            start++;
        }
    }

}


//命令行解析器
int main()
{
    //命令行解释器要一直运行，得死循环
    while(1)
    {
        //初始化
        redirType=NONE_REDIR;
        redirFile=NULL;
        errno=0;


        //打印提示符
        printf("用户名@主机名 当前路径# ");
        fflush(stdout);


        //获取用户输入的命令,输入的时候，我们在键盘中会输入最后一定会按回车\n,所以我们在这里不需要\n
        char* s=fgets(LineCommand,sizeof(LineCommand)-1,stdin);
        assert(s!=NULL);
        //所以这里我们会多出一个\n,要清除最后一个\n
        LineCommand[strlen(LineCommand)-1]=0;
        //printf("%s\n",LineCommand);
        
        //"ls -a -l -i" -> "ls" "-a" "-l" "-i" -> 1->多
        //输出重定向："ls -a -l -i > myfile.txt" -> "ls -a -l -i" "myfile.txt"
        //追加重定向："ls -a -l -i >> myfile.txt" -> "ls -a -l -i" "myfile.txt"
        //输入重定向："cat < myfile.txt" -> "cat" "myfile.txt"
        //如何把上面的命令按>/>>/<两边进行分割呢？让它们先为\0,就可以以>/>>/<为中心分成两部分了
        commandCheck(LineCommand); //在外面写一个扫描命令的函数

        
        //需要字符串切割——c语言字符串切割可以用strtok函数
        //第一个参数为要切割的字符串，第二个参数为分隔符
        myargv[0]=strtok(LineCommand," ");
        //切割时第二次如果还想在原始字符串中做切割，第一个参数得传NULL
        //如果没有字串了，strtok会返回NULL，正好我们的myargv[end]也要为NULL
        int i=1;
        if(myargv[0]!=NULL && strcmp(myargv[0],"ls")==0)
        {
            myargv[i++]="--color=auto";
        }
        while(myargv[i++]=strtok(NULL," "));

        //如果是cd命令，不需要创建子进程，让shell自己执行对应的命令，本质就是执行系统接口
        //像这种不需要让我们子进程来执行，而是让shell自己来执行的命令 ——内建/内置命令—
        if(myargv[0]!=NULL&&strcmp(myargv[0],"cd")==0)
        {
            if(myargv[1]!=NULL)
            {
                chdir(myargv[1]);
            }
            continue;
        }

        if(myargv[0]!=NULL&&myargv[1]!=NULL&&strcmp(myargv[0],"echo")==0)
        {
            if(strcmp(myargv[1],"$?")==0)
            {
                //输入echo $?命令后显示最近一次退出进程的退出码和退出信号
                printf("退出码：%d，退出信号：%d\n",lastCode,lastSig);
            }
            else 
            {
                printf("%s\n",myargv[1]);
            }
            continue;
        }



        //测试是否成功,条件编译,这样的话如果我们不想测试了，就直接在Makefile中把宏DEBUG注释掉就好
        //myargv[i]在最后为NULL，为假进而退出for循环
     #ifdef DEBUG 
        for(int i=0;myargv[i];i++)
        {
        printf("myargv[%d]: %s\n",i,myargv[i]);
        }
     #endif


        //执行命令——让子进程,因为我们这个命令行解释器进程的代码不能被覆盖
        pid_t id=fork();
        assert(id!=-1);
        if(id==0)
        {
            //因为命令是子进程执行的，真正重定向的工作一定要是子进程来完成
            //但是如何重定向是父进程要给子进程提供信息的，因为命令行解析不是由子进程来做的
            //我们已经在一开始写好了全局变量来记录重定向的信息了，子进程和父进程会共享这部分信息
            //那么这里的重定向会影响父进程吗？
            //答：不会，进程具有独立性，子进程有自己拷贝的file_struct，不会影响父进程，但是我们打开的文件是不需要给子进程拷贝一份的


            //这里用switch选择语句来依次重定向
            switch(redirType)
            {
                case NONE_REDIR:
                    //什么都不做
                    break;
                case INPUT_REDIR:
                    {
                        int fd=open(redirFile,O_RDONLY);
                        if(fd<0)
                        {
                            perror("open");
                            exit(errno);//直接退出子进程
                        }

                        //重定向的文件已经成功打开，接下来进行输入重定向
                        dup2(fd,0);
                    }
                    break;

                //让输出和追加重定向都匹配到它
                case OUTPUT_REDIR:
                case APPEND_REDIR:
                    {
                        umask(0);
                        int flags=O_WRONLY|O_CREAT;
                        if(redirType==APPEND_REDIR)
                        {
                            flags|=O_APPEND; //追加
                        }
                        else 
                        {
                            flags|=O_TRUNC; //刷新
                        }

    
                        int fd=open(redirFile,flags,0666);
                        if(fd<0)
                        {
                            perror("open");
                            exit(errno);
                        }

                        //接下来进行输出/追加重定向
                        dup2(fd,1);


                    }
                    break;

                default:
                    printf("bug?\n");
                    break;

            }
            


            //执行程序替换的时候，会不会影响曾经进程打开的重定向的文件？答：不会，替换的是数据和代码，和内核数据结构没关系 
            //这里用execvp来加载,myargv[i]为要执行的程序，myargv为要执行的参数数组
            execvp(myargv[0],myargv);
            exit(0);
        }

        int status=0;
        pid_t ret=waitpid(id,&status,0);
        assert(ret>0);
        lastCode=((status>>8)&0xFF);
        lastSig=(status&0x7F);
        

    }


    return 0;
}
